//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.deploy.test;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.UnknownHostException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.stream.Collectors;

import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenPaths;
import org.eclipse.jetty.toolchain.test.PathMatchers;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.eclipse.jetty.xml.XmlConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * Allows for setting up a Jetty server for testing based on XML configuration files.
 */
public class XmlConfiguredJetty
{
    private static final Logger LOG = LoggerFactory.getLogger(XmlConfiguredJetty.class);
    private final List<Resource> _xmlConfigurations = new ArrayList<>();
    private final Map<String, String> _properties = new HashMap<>();
    private Server _server;
    private ContextHandlerCollection _contexts;
    private int _serverPort;
    private String _scheme = HttpScheme.HTTP.asString();
    private Path _jettyBase;

    /**
     * Configure for the list of XML Resources and Properties.
     *
     * @param xmls the xml resources (in order of execution)
     * @param properties the properties to use with the XML
     * @return the ID Map of configured objects (key is the id name in the XML, and the value is configured object)
     * @throws Exception if unable to create objects or read XML
     */
    public static Map<String, Object> configure(List<Resource> xmls, Map<String, String> properties, Path jettyBase) throws Exception
    {
        Map<String, Object> idMap = new HashMap<>();

        Path testConfig = jettyBase.resolve("xml-configured-jetty.properties");
        try (OutputStream out = Files.newOutputStream(testConfig))
        {
            properties.put("jetty.deploy.common.properties", testConfig.toString());

            // Write out configuration for use by ConfigurationManager.
            Properties props = new Properties();
            props.putAll(properties);
            props.store(out, "Generated by " + XmlConfiguredJetty.class.getName());
        }

        // Configure everything
        for (Resource xmlResource : xmls)
        {
            try
            {
                XmlConfiguration configuration = new XmlConfiguration(xmlResource);
                configuration.getIdMap().putAll(idMap);
                configuration.getProperties().putAll(properties);
                configuration.configure();
                idMap.putAll(configuration.getIdMap());
            }
            catch (Exception e)
            {
                throw new IllegalStateException("Unable to configure XML: " + xmlResource, e);
            }
        }

        return idMap;
    }

    public XmlConfiguredJetty(Path testDir) throws IOException
    {
        _jettyBase = testDir.resolve("base");
        FS.ensureDirExists(_jettyBase);

        setProperty("jetty.base", _jettyBase.toString());

        Path logsDir = _jettyBase.resolve("logs");
        FS.ensureDirExists(logsDir);

        Path webappsDir = _jettyBase.resolve("webapps");
        FS.ensureEmpty(webappsDir);
        setProperty("jetty.deploy.webappsDir", webappsDir.toString());
        setProperty("jetty.deploy.scanInterval", "1");

        Path etcDir = _jettyBase.resolve("etc");
        FS.ensureDirExists(etcDir);

        Files.copy(MavenPaths.findTestResourceFile("etc/realm.properties"), etcDir.resolve("realm.properties"));
        Files.copy(MavenPaths.findTestResourceFile("etc/webdefault.xml"), etcDir.resolve("webdefault.xml"));

        Path tmpDir = _jettyBase.resolve("tmp");
        FS.ensureEmpty(tmpDir);
        System.setProperty("java.io.tmpdir", tmpDir.toString());
        setProperty("jetty.deploy.tempDir", tmpDir.toString());
    }

    public void addConfiguration(Path xmlConfigFile)
    {
        addConfiguration(ResourceFactory.root().newResource(xmlConfigFile));
    }

    public void addConfiguration(Resource xmlConfig)
    {
        _xmlConfigurations.add(xmlConfig);
    }
    
    public List<Resource> getConfigurations()
    {
        return Collections.unmodifiableList(_xmlConfigurations);
    }

    public void assertNoContextHandlers()
    {
        int count = _contexts.getHandlers().size();
        assertEquals(0, count, "Should have no Contexts, but saw [%s]".formatted(_contexts.getHandlers().stream().map(Handler::toString).collect(Collectors.joining(", "))));
    }

    public String getResponse(String path) throws IOException
    {
        URI destUri = getServerURI().resolve(path);
        URL url = destUri.toURL();

        URLConnection conn = url.openConnection();
        conn.addRequestProperty("Connection", "close");
        try (InputStream in = conn.getInputStream())
        {
            return IO.toString(in);
        }
    }

    public void assertResponseContains(String path, String needle) throws IOException
    {
        String content = getResponse(path);
        assertThat(content, containsString(needle));
    }

    public void assertContextHandlerExists(String... expectedContextPaths)
    {
        if (expectedContextPaths.length != _contexts.getHandlers().size())
        {
            StringBuilder failure = new StringBuilder();
            failure.append("## Expected Contexts [%d]\n".formatted(expectedContextPaths.length));
            for (String expected : expectedContextPaths)
            {
                failure.append(" - ").append(expected).append('\n');
            }
            failure.append("## Actual Contexts [%d]\n".formatted(_contexts.getHandlers().size()));
            _contexts.getHandlers().forEach((handler) -> failure.append(" - ").append(handler).append('\n'));
            assertEquals(expectedContextPaths.length, _contexts.getHandlers().size(), failure.toString());
        }

        for (String expectedPath : expectedContextPaths)
        {
            boolean found = false;
            for (Handler handler : _contexts.getHandlers())
            {
                if (handler instanceof ContextHandler contextHandler)
                {
                    if (contextHandler.getContextPath().equals(expectedPath))
                    {
                        found = true;
                        assertThat("Context[" + contextHandler.getContextPath() + "].state", contextHandler.getState(), is("STARTED"));
                        break;
                    }
                }
            }
            assertTrue(found, "Did not find Expected Context Path " + expectedPath);
        }
    }

    public ContextHandler getContextHandler(String contextPath)
    {
        ContextHandler contextHandler = null;
        for (Handler handler : _contexts.getHandlers())
        {
            if (handler instanceof ContextHandler ch)
            {
                if (ch.getContextPath().equals(contextPath))
                {
                    contextHandler = ch;
                    break;
                }
            }
        }
        return contextHandler;
    }

    private void copyFile(String type, Path srcFile, Path destFile) throws IOException
    {
        assertThat(srcFile, PathMatchers.isRegularFile());
        Files.copy(srcFile, destFile, StandardCopyOption.REPLACE_EXISTING);
        assertThat(destFile, PathMatchers.isRegularFile());
        System.err.printf("Copy %s: %s%n  To %s: %s%n", type, srcFile, type, destFile);
    }

    public void copyWebapp(String srcName, String destName) throws IOException
    {
        System.err.printf("Copying Webapp: %s -> %s%n", srcName, destName);
        Path srcFile = MavenPaths.findTestResourceFile("webapps/" + srcName);
        Path destFile = _jettyBase.resolve("webapps/" + destName);

        copyFile("Webapp", srcFile, destFile);
    }

    public String getScheme()
    {
        return _scheme;
    }

    public Server getServer()
    {
        return _server;
    }

    public int getServerPort()
    {
        return _serverPort;
    }

    public URI getServerURI() throws UnknownHostException
    {
        return HttpURI.from(getScheme(), InetAddress.getLocalHost().getHostAddress(), getServerPort(), null).toURI();
    }

    public Path getJettyBasePath()
    {
        return _jettyBase;
    }

    public Server load() throws Exception
    {
        Map<String, Object> idMap = configure(_xmlConfigurations, _properties, _jettyBase);

        Server server = (Server)idMap.get("Server");

        this._server = Objects.requireNonNull(server, "Load failed to configure a " + Server.class.getName());
        this._server.setStopTimeout(1000); //wait up to a second to stop

        ContextHandlerCollection contexts = (ContextHandlerCollection)idMap.get("Contexts");
        this._contexts = Objects.requireNonNull(contexts, "Load failed to configure a " + ContextHandlerCollection.class.getName());

        return this._server;
    }

    public void removeWebapp(String name) throws IOException
    {
        Path webappFile = _jettyBase.resolve("webapps/" + name);
        if (Files.exists(webappFile))
        {
            LOG.info("Removing webapp: {}", webappFile);
            Files.delete(webappFile);
        }
    }

    public void setProperty(String key, String value)
    {
        _properties.put(key, value);
    }

    public void setScheme(String scheme)
    {
        this._scheme = URIUtil.normalizeScheme(scheme);
    }

    public void start() throws Exception
    {
        assertNotNull(_server, "Server should not be null (failed load?)");

        _server.start();

        // Find the active server port.
        _serverPort = _server.getURI().getPort();

        // Uncomment to have server start and continue to run (without exiting)
        // System.err.printf("Listening to port %d%n",this.serverPort);
        // server.join();
    }

    public void stop() throws Exception
    {
        LifeCycle.stop(_server);
        _properties.clear();
    }
}
